/* Gauge - Performance Measurement Framework for Mobile Platforms
 * Copyright (C) 2013, Juha Hytonen <juha.a.hytonen@gmail.com>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package org.gauge;

import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.Socket;
import java.util.ArrayList;

import org.gauge.SocketMonitor.SocketListener;

/**
 * The base class for connection managers used in Agent and Server.
 * 
 * Connection manager handles establishing a connection to and transferring
 * data to and from the remote end.
 */
public abstract class AbstractConnectionManager implements Runnable, SocketListener {

	/**
	 * Callback for connection manager.
	 */
    public interface ConnectionManagerCb {
        
    	/**
    	 * Connection to remote end was established.
    	 */
        public void ConnectedEvent();
        
        /**
         * Connection manager was disconnected from remote end.
         */
        public void DisconnectedEvent();
        
        /**
         * Data received from remote end.
         */
        public void DataEvent(String pData);
        
        /**
         * Connection attempt to remote end failed.
         */
        public void ConnectionFailedEvent();
        
    }
    
    public AbstractConnectionManager() {
        mCbs = new ArrayList<ConnectionManagerCb>();
        initialize();
    }
    
    private void initialize() {
        mQuit = false;
        mClose = false;
        mOutputBuffer = new StringBuilder();
    }
    
    public final void AddCallback(ConnectionManagerCb pCb) {
        mCbs.add(pCb);
    }
    
    public void Start() {
        initialize();
    	mThread = new Thread(this);
    	mThread.setDaemon(true);
    	mThread.start();
    }

    synchronized public final void Send(String pData) {
        synchronized(mOutputBuffer) {
            mOutputBuffer.append(pData);
            mOutputBuffer.append(Protocol.MsgSeparator);
        }
        notify();
    }

    synchronized public final void Disconnect() {
    	if (mThread == null) {
    		return;
    	}
    	
        mClose = true;
        notify();
    }
    
    // Runnable ////////////////////////////////////////////////////////////////
    
    public void run() {
        try {
            // While accepting new connections...
            while (!mQuit) {
                // Create socket and connection. - IOException possible.
                Socket socket = acquireSocket();

                // Notify callbacks of connection.
                for(ConnectionManagerCb cb : mCbs) {
                    cb.ConnectedEvent();
                }
                onConnection(socket);

                // Hand over to socket processing.
                handleSocket(socket);

                // Notify callbacks that socket has disconnected.
                for(ConnectionManagerCb cb : mCbs) {
                    cb.DisconnectedEvent();
                }
                onConnectionLost();
            }
        }
        catch(IOException e) {
            for(ConnectionManagerCb cb : mCbs) {
                cb.ConnectionFailedEvent();
            }
        }
        finally {
        	mThread = null;
        }
    }

    // SocketListener ////////////////////////////////////////////////////////

    synchronized public final void SocketEvent() {
        notify();
    }

    synchronized public final void SocketLostEvent() {
        mClose = true;
        notify();
    }

    protected final void quit() {
        mQuit = true;
    }

    protected void onConnection(Socket pSocket) {
    }

    protected void onConnectionLost() {
    }

    protected abstract Socket acquireSocket() throws IOException;

    // Private parts. ////////////////////////////////////////////////////////

    private Thread mThread;
    private ArrayList<ConnectionManagerCb> mCbs;
    private boolean mQuit;
    private boolean mClose;
    private StringBuilder mOutputBuffer;

    private void handleSocket(Socket pSocket) {
        mClose = false;
        try {
            // Initialize streams. - IOException possible
            InputStreamReader input = new InputStreamReader(pSocket.getInputStream());
            OutputStreamWriter output = new OutputStreamWriter(pSocket.getOutputStream());

            // Buffer for reading data from server.
            StringBuilder inputBuffer = new StringBuilder();
            while(!mClose) {
                synchronized(this) { 
                    try {
                        // Start watching input stream.
                        SocketMonitor monitor = new SocketMonitor(this, pSocket);
                        monitor.start();

                        // Wait for close request or data.
                        wait();

                        writeData(output);
                        readData(input, inputBuffer);
                    }
                    catch(InterruptedException e) { // wait()
                    }
                }
            }

            // Close socket.
            pSocket.close();
        }
        catch(IOException e) {
        }
    }

    private void writeData(OutputStreamWriter pOutput) {
        synchronized(mOutputBuffer) {
            if(mOutputBuffer.length() == 0) {
                return;
            }

            try {
                for(int i = 0; i < mOutputBuffer.length(); i++) {
                    pOutput.write(mOutputBuffer.charAt(i));
                }
                pOutput.flush();
            }
            catch(IOException e) {
            }

            mOutputBuffer.delete(0, mOutputBuffer.length());
        }
    }
    
    private void readData(InputStreamReader pInput, StringBuilder pBuffer) {
        try {
            // Read data from server.
            while(!mClose) {
                if(!pInput.ready()) {
                    break;
                } // No else.

                char c = (char)pInput.read();
                if(c == -1) {
                    break;
                }
                else if(c != Protocol.PollCharacter) {
                    if (c == Protocol.MsgSeparator) {
                        // End of message.
                        for(ConnectionManagerCb cb : mCbs) {
                            cb.DataEvent(pBuffer.toString());
                        }
                        pBuffer.delete(0, pBuffer.length());
                    }
                    else {
                        pBuffer.append(c);
                    }
                } // else c == PollCharacter, ignore
            }
        }
        catch(IOException e) {
        }
    }

}
